package com.dny.asmtop.op;

import com.dny.asmtop.Command;
import com.dny.asmtop.MethodContext;
import com.dny.asmtop.Preconditions;
import jdk.internal.org.objectweb.asm.Label;
import jdk.internal.org.objectweb.asm.Type;
import jdk.internal.org.objectweb.asm.commons.GeneratorAdapter;
import jdk.internal.org.objectweb.asm.commons.Method;

import java.util.Objects;

import static com.dny.asmtop.ASMMethodUtils.isPrimitiveType;
import static com.dny.asmtop.op.PredicateDefCmp.Operation.*;
import static jdk.internal.org.objectweb.asm.Type.BOOLEAN_TYPE;
import static jdk.internal.org.objectweb.asm.Type.INT_TYPE;

/**
 * Created by jlutt on 2018-01-16.
 *
 * @author jlutt
 */
public class PredicateDefCmp implements PredicateDef {

  private Command left;
  private Command right;
  private Operation operation = EQ;

  public enum Operation {
    EQ(GeneratorAdapter.EQ, "=="),
    NE(GeneratorAdapter.NE, "!="),
    LT(GeneratorAdapter.LT, "<"),
    GT(GeneratorAdapter.GT, ">"),
    LE(GeneratorAdapter.LE, "<="),
    GE(GeneratorAdapter.GE, ">=");

    private final int opCode;
    private final String symbol;

    Operation(int opCode, String symbol) {
      this.opCode = opCode;
      this.symbol = symbol;
    }
  }

  private PredicateDefCmp(Operation operation, Command left, Command right) {
    this.left = left;
    this.right = right;
    this.operation = operation;
  }

  public static PredicateDefCmp create(Operation operation, Command left, Command right) {
    return new PredicateDefCmp(operation, left, right);
  }

  @Override
  public Type type(MethodContext context) {
    return BOOLEAN_TYPE;
  }

  @Override
  public Type generator(MethodContext context) {
    GeneratorAdapter g = context.getGeneratorAdapter();
    Label labelTrue = new Label();
    Label labelExit = new Label();

    Type leftFieldType = left.type(context);
    Preconditions.check(leftFieldType.equals(right.type(context)));
    left.generator(context);
    right.generator(context);

    if (isPrimitiveType(leftFieldType)) {
      g.ifCmp(leftFieldType, operation.opCode, labelTrue);
    } else {
      if (operation == EQ || operation == NE) {
        g.invokeVirtual(leftFieldType, new Method("equals", BOOLEAN_TYPE, new Type[]{Type.getType(Object.class)}));
        g.push(operation == EQ);
        g.ifCmp(BOOLEAN_TYPE, GeneratorAdapter.EQ, labelTrue);
      } else {
        g.invokeVirtual(leftFieldType, new Method("compareTo", INT_TYPE, new Type[]{Type.getType(Object.class)}));
        if (operation == LT) {
          g.ifZCmp(GeneratorAdapter.LT, labelTrue);
        } else if (operation == GT) {
          g.ifZCmp(GeneratorAdapter.GT, labelTrue);
        } else if (operation == LE) {
          g.ifZCmp(GeneratorAdapter.LE, labelTrue);
        } else if (operation == GE) {
          g.ifZCmp(GeneratorAdapter.GE, labelTrue);
        }
      }
    }

    g.push(false);
    g.goTo(labelExit);

    g.mark(labelTrue);
    g.push(true);

    g.mark(labelExit);

    return BOOLEAN_TYPE;
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    PredicateDefCmp that = (PredicateDefCmp) o;
    return Objects.equals(left, that.left) &&
        Objects.equals(right, that.right) &&
        operation == that.operation;
  }

  @Override
  public int hashCode() {
    return Objects.hash(left, right, operation);
  }
}
